package club.jdiy.core.storage;

import club.jdiy.core.properties.JDiyStoreProperties;
import club.jdiy.utils.FileUtils;
import club.jdiy.utils.IdWorker;
import club.jdiy.utils.Md5Utils;
import club.jdiy.utils.StringUtils;
import io.minio.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.regex.Pattern;

/**
 * Minio对象存储实现类.
 *
 * @author ziquee(子秋) QQ号:39886616 主页: http://www.jdiy.club
 * @version 20220307
 */
@SuppressWarnings("ALL")
@Slf4j
public class MinioStore extends StoreSaver implements Store {
    private static MinioClient minioClient;
    private static JDiyStoreProperties.Minio oss;
    private final String storeBucket;
    private final String dataId;
    private StoreMetaData storeMetaData;

    public synchronized static void init(JDiyStoreProperties.Minio oss) {
        MinioStore.oss = oss;
        if (minioClient == null) {
            minioClient = MinioClient.builder()
                    .endpoint(oss.getEndpoint()).credentials(oss.getAccessKey(), oss.getSecretKey()).build();
        }
        String s = oss.getDomain();
        if (!s.endsWith("/")) s += "/";
        oss.setDomain(s);
    }

    public MinioStore(JDiyStoreProperties.Minio oss, StoreMetaData storeMetaData, String storeBucket, String id) {
        if (minioClient == null) init(oss);
        this.storeMetaData = storeMetaData;
        this.storeBucket = storeBucket;
        this.dataId = id;
    }


    public StoreMetaData getStoreData() {
        return storeMetaData;
    }

    @Override
    String getDataId() {
        return dataId;
    }

    /**
     * view层调用核心方法．使用方式：
     * 1. DBEntity继承 {@link AbstractStorble} 或实现 {@link Storable}接口.
     * 2. 然后在Controller中调用 DBEntity对象的.init方法
     * 3. view中取得单一附件（例如获取listPic附件的下载地址）:   ${vo.store.listPic}  或者：  ${vo.store['listPic']}
     * view中取得多个附件（例如获取listPic附件的下载地址）:  ${vo.store.listPic.items[0]}
     *
     * @param field 附件字段名．对应于 ${vo.store.xxx}中的xxx
     * @return Entry
     */
    //不要更改和移除(view层调用核心方法),
    // 可以直接使用 store[field]来获取对应的Attch
    //获取单个附件字段的Store
    public Entry get(String field) {
        List<StoreMetaData.StoreItem> aiit = storeMetaData.getItems().get(field);
        if (aiit == null) return new Entry(new EntryItem[0]);

        EntryItem[] entries = new EntryItem[aiit.size()];
        int i = 0;
        for (StoreMetaData.StoreItem ai : aiit) {
            entries[i++] = new EntryItem(ai, oss.getDomain() + ai.getKey());
        }
        return new Entry(entries);
    }

    public String getUrl(String field) {
        List<StoreMetaData.StoreItem> aiit = storeMetaData.getItems().get(field);
        return aiit == null || aiit.size() < 1 ? null : oss.getDomain() + aiit.get(0).getKey();
    }

    public String[] getUrls(String field) {
        List<StoreMetaData.StoreItem> aiit = storeMetaData.getItems().get(field);
        if (aiit == null || aiit.size() < 1) return null;
        int i = 0;
        String[] urls = new String[aiit.size()];
        for (StoreMetaData.StoreItem ai : aiit) urls[i++] = oss.getDomain() + ai.getKey();
        return urls;
    }

    public void set(String field, File file) throws IOException {
        this.set(field, file, null);
    }

    public void set(String field, File file, String fileName) throws IOException {
        __saveFile(field, file, fileName, false);
    }

    public void add(String field, File file) throws IOException {
        this.add(field, file, null);
    }

    public void add(String field, File file, String fileName) throws IOException {
        __saveFile(field, file, fileName, true);
    }

    public void set(String field, MultipartFile mFile) throws IOException {
        this.set(field, mFile, null);
    }

    public void set(String field, MultipartFile mFile, String fileName) throws IOException {
        this.set(field, mFile, fileName, null);
    }

    /**
     * @param field    文件字段名
     * @param mFile    上传附件
     * @param allowExt 　逗号分隔的文件扩展名列表，为null不限制
     * @param c        　用户可对保存的文件进行一定的处理（如图片加水印，压缩尺寸等）
     * @return 当前持久化保存的附件
     * @throws IOException IO
     */
    public void set(String field, MultipartFile mFile, String fileName, Consumer<File> c) throws IOException {
        __saveFile(field, mFile, fileName, false, c);
    }

    /**
     * @param field    文件字段名
     * @param mFile    上传附件
     * @param allowExt 　逗号分隔的文件扩展名列表，为null不限制
     * @return 当前持久化保存的附件
     * @throws IOException IO
     */
    public void add(String field, MultipartFile mFile) throws IOException {
        __saveFile(field, mFile, null, true, null);
    }

    public void add(String field, MultipartFile mFile, String fileName) throws IOException {
        this.add(field, mFile, fileName, null);
    }

    public void add(String field, MultipartFile mFile, String fileName, Consumer<File> c) throws IOException {
        __saveFile(field, mFile, fileName, true, c);
    }

    public void del(String field) {
        if (field == null || "".equals(field)) return;
        List<StoreMetaData.StoreItem> aiit = storeMetaData.getItems().get(field);
        if (aiit != null) {
            for (StoreMetaData.StoreItem ai : aiit) remove(ai.getKey());
            storeMetaData.getItems().remove(field);
        }
    }

    public void del(String field, String key) {
        List<StoreMetaData.StoreItem> aiit = storeMetaData.getItems().get(field);
        if (aiit != null) {
            for (StoreMetaData.StoreItem ai : aiit) {
                if (ai.getKey().equals(key)) {
                    remove(key);
                    aiit.remove(ai);
                    break;
                }
            }
            if (aiit.size() < 1) storeMetaData.getItems().remove(field);
        }
    }

    public void delAll() {
        for (List<StoreMetaData.StoreItem> aiit : storeMetaData.getItems().values()) {
            if (aiit != null) {
                for (StoreMetaData.StoreItem ai : aiit) remove(ai.getKey());
            }
        }
        storeMetaData.setItems(new HashMap<>());
    }

    @Override
    public String put(MultipartFile file, String fileName) throws IOException {
        return put(file, fileName, null);
    }

    @Override
    public String put(MultipartFile file, String fileName, Consumer<File> c) throws IOException {
        String field = IdWorker.getId() + "";
        __saveFile(field, file, fileName, true, c);
        return getUrl(field);
    }

    @Override
    public String put(File file, String fileName) throws IOException {
        String field = IdWorker.getId() + "";
        set(field, file, fileName);
        return getUrl(field);
    }

    @Override
    public String put(byte[] bytes, String fileName) throws IOException {
        File tmpFile = new File(FileUtils.getTempDirectory(), IdWorker.getId() + ".tmp");
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(tmpFile));
        bos.write(bytes);
        bos.flush();
        bos.close();

        return put(tmpFile, fileName);
    }

    private void __saveFile(String field, File file, String fileName, boolean isAdd) throws IOException {
        if (file == null || field == null || "".equals(field) || !file.exists() || !file.isFile()) return;
        boolean autoRename = fileName == null || "".equals(fileName);
        if (autoRename) fileName = file.getName();
        String ext = file.getName().substring(file.getName().lastIndexOf(".") + 1).toLowerCase();
        if (!fileName.toLowerCase().endsWith("." + ext)) fileName = fileName + "." + ext;

        if (autoRename && !Pattern.matches("^[a-zA-Z0-9_\\-.~@()]+$", fileName)) {
            if ("flv".equals(ext) || "jpg".equals(ext) || "jpeg".equals(ext) || "png".equals(ext)) {
                fileName = Md5Utils.md5(fileName).substring(8, 24) + "." + ext;//这些扩展名需要强制更名.注意要幂等，同一文件再次上传直接覆盖已上传的同名文件
            }
        }
        if (!isAdd) del(field);
        String key = save(field, fileName, file, null);
        boolean has = false;
        List<StoreMetaData.StoreItem> aiit = storeMetaData.getItems().get(field);
        if (aiit != null) {
            for (StoreMetaData.StoreItem ai : aiit) {
                if (ai.getKey().equals(key)) {
                    has = true;
                    break;
                }
            }
        } else {
            storeMetaData.getItems().put(field, new ArrayList<>());
        }
        if (!has) storeMetaData.getItems().get(field).add(new StoreMetaData.StoreItem(key, file.length(), ext));
    }

    private void __saveFile(String field, MultipartFile mFile, String fileName, boolean isAdd, Consumer<File> c) throws IOException {
        if (mFile == null || mFile.isEmpty()) return;
        if (fileName == null || "".equals(fileName)) fileName = mFile.getOriginalFilename();
        String ext = mFile.getOriginalFilename().substring(mFile.getOriginalFilename().lastIndexOf(".") + 1).toLowerCase();
        if (!fileName.toLowerCase().endsWith("." + ext)) fileName = fileName + "." + ext;

        if (!Pattern.matches("^[a-zA-Z0-9_\\-.~@()]+$", fileName)) {
            if ("flv".equals(ext) || "jpg".equals(ext) || "jpeg".equals(ext) || "png".equals(ext)) {
                fileName = Md5Utils.md5(fileName).substring(8, 24) + "." + ext;//这些扩展名需要强制更名.注意要幂等，同一文件再次上传直接覆盖已上传的同名文件
            }
        }

        if (!isAdd) del(field);
        String key;
        if (c == null) {
            key = save(field, fileName, mFile);
        } else {
            File dir = new File("/tmp/" + UUID.randomUUID());
            dir.mkdirs();
            File file = new File(dir.getAbsolutePath() + "/" + fileName);
            try (InputStream inputStream = mFile.getInputStream(); OutputStream outputStream = new FileOutputStream(file)) {
                byte[] bs = new byte[1024];
                int res;
                while ((res = inputStream.read(bs)) != -1) {
                    outputStream.write(bs, 0, res);
                }
                outputStream.flush();
            }
            c.accept(file);
            key = save(field, fileName, file, mFile.getContentType());
            file.delete();
            FileUtils.delEmpty(file.getParentFile());
        }
        boolean has = false;
        List<StoreMetaData.StoreItem> aiit = storeMetaData.getItems().get(field);
        if (aiit != null) {
            for (StoreMetaData.StoreItem ai : aiit) {
                if (ai.getKey().equals(key)) {
                    has = true;
                    break;
                }
            }
        } else {
            storeMetaData.getItems().put(field, new ArrayList<>());
        }
        if (!has) storeMetaData.getItems().get(field).add(new StoreMetaData.StoreItem(key, mFile.getSize(), ext));
    }


    private void remove(String key) {
        try {
            minioClient.removeObject(RemoveObjectArgs.builder().bucket(oss.getBucket()).object(key).build());
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }


    private void createBucket(MinioClient minioClient, String bucket) throws IOException {
        try {

            if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build())) {
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build());
                log.debug("Create a new Bucket:" + bucket);
            } else {
                log.error("Bucket has been exisit:" + bucket);
            }

        } catch (Exception e) {
            throw new IOException(e.getMessage(), e.getCause());
        }
    }

    private String save(String field, String fileName, File file, String contentType) throws IOException {
        String directory = storeBucket + "/" + dataId + "/" + field;
        String key = directory + "/" + fileName;
        this.createBucket(minioClient, oss.getBucket());
        try (InputStream inputStream = new FileInputStream(file)) {
            // PutObjectArgs poa = PutObjectArgs.builder().bucket(oss.getBucket());
            PutObjectArgs.Builder builder = PutObjectArgs.builder().bucket(oss.getBucket()).object(key);

            if (StringUtils.isEmpty(contentType)) {
                try {
                    contentType = FileUtils.getContentType(file.getAbsolutePath());
                } catch (Exception ignore) {
                }
            }
            if (StringUtils.hasText(contentType)) {
                try {
                    builder.contentType(contentType);
                } catch (Exception ignore) {
                }
            }

            minioClient.putObject(builder
                    .stream(inputStream, file.length(), -1).build());
            inputStream.close();
        } catch (Exception e) {
            throw new IOException(e.getMessage(), e.getCause());
        }
        return key;
    }

    private String save(String field, String fileName, MultipartFile multipartFile) throws IOException {
        String directory = storeBucket + "/" + dataId + "/" + field;
        String key = directory + "/" + fileName;
        this.createBucket(minioClient, oss.getBucket());
        try (InputStream inputStream = multipartFile.getInputStream()) {
            minioClient.putObject(PutObjectArgs.builder().bucket(oss.getBucket()).object(key).
                    contentType(multipartFile.getContentType()).
                    stream(inputStream, multipartFile.getSize(), -1).build());
            inputStream.close();
        } catch (Exception e) {
            throw new IOException(e.getMessage(), e.getCause());
        }
        return key;
    }

}

